package grex;

import grex.ErrorManagement.ErrorManager;
import grex.Nodes.Node;
import grex.Nodes.TargetPred;
import grex.fitnessfunctions.ParsimonyPressure;
import grex.Nodes.GeneException;

import java.util.*;

/**
 * <p>Title: </p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2003</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */

import java.io.Serializable;

import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Population implements Serializable {
    private static final int NR_OF_CROSSOVER_TRIES = 20;
    private GP[] population;
    private double populationFitness = 0;
    private double populationLength = 0;
    private double populationInvertedFitness =
        0; //used for calculating the porportional fitness sinze this is based on ACC and not error
    private int size;
    private Environment environment;
    private Options options;
    //Fix parralization of fitness function each objectneeds it own function
    private final int CPUs = Runtime.getRuntime().availableProcessors();


    public Population(int size, Environment environment) throws BNFException,
                                                                GeneException {
        this.environment = environment;
        this.size = size;
        this.options = environment.getOptions();
        createPopulation(size);
        System.out.println("Using " + CPUs + " CPUs");
        
        calcPopulationFitness();

    }
    //Anvonds nor et population skapas med ett GP som en mall

    public Population(int size, GP gp,
                      Environment environment) throws BNFException,
                                                      GeneException {
        this.environment = environment;
        this.options = environment.getOptions();
        this.size = size;
        createPopulation(size, gp);
        System.out.println("Using " + CPUs + " CPUs");
        calcPopulationFitness();
    }

    public Population(GP[] population,
                      Environment environment) throws GeneException,
                                                      BNFException {
        this.population = population;
        this.environment = environment;
        this.options = environment.getOptions();
        size = population.length;
        System.out.println("Using " + CPUs + " CPUs");
        calcPopulationFitness();
    }

    public void injectGP(GP gp, int nrOfMutadedTrees) throws BNFException, GeneException {
        for (int i = 0; i < nrOfMutadedTrees+1; i++) {
            GP gp1 = (GP)gp.clone();
            for (int j = 0; j < i; j++)
                mutate(gp1);
            gp1.calcAdjustedFitness();
            population[1+(Options.rnd.nextInt(population.length)-1)] = gp1;
        }

    }
    
    ArrayList<GP> firstGeneration;
    private class CreateGPTask implements Runnable,Serializable {
        Environment env;
        
        int cType, cDepth;
        public CreateGPTask(Environment environment, int creationType, int creationDepth) {
            env=environment;
            cType=creationType;
            cDepth = creationDepth;                                       
        }


        public void run() {
            try {
                firstGeneration.add(new GP(environment, cType, cDepth));
            } catch (BNFException ex) {
                Logger.getLogger(Population.class.getName()).log(Level.SEVERE, null, ex);
            } catch (GeneException ex) {
                Logger.getLogger(Population.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    

    private void createPopulation(int size, GP gp,
                                  Environment environment) throws BNFException,
                                                                  GeneException {
        this.environment = environment;
        this.options = environment.getOptions();
        createPopulation(size, gp, environment);
    }

    private void createPopulation(int size, GP gp) throws BNFException,
                                                          GeneException {
        this.size = size;
        this.options = gp.getOptions();
        population = new GP[size];


        population[0] = (GP)gp.clone();
        population[0].calcAdjustedFitness();
        
        int ratioOfNewTrees = 1;
        if (size > 1) {
            population[1] = (GP)gp.clone();
            population[1].calcAdjustedFitness();
            for (int i = 2; i < population.length / ratioOfNewTrees; i++) {
                population[i] =
                        new GP(environment, Options.GROW, options.getMAX_CREATION_DEPTH());
            }
            for (int i = population.length / ratioOfNewTrees;
                 i < (population.length); i++) {
                population[i] = (GP)gp.clone();
            }
            for (int i = population.length / 4; i < population.length - 1;
                 i++) {
                mutate(population[i]);
            }
            for (int i = (population.length / 4) * 2;
                 i < population.length - 1; i++) {
                mutate(population[i]);
            }
            for (int i = (population.length / 4) * 3;
                 i < population.length - 1; i++) {
                mutate(population[i]);
            }
            for (int i = 0; i < population.length - 1; i++) {
                population[i].calcAdjustedFitness();
            }
        }
    }

   /* private void createPopulation(int size) throws BNFException,
                                                   GeneException {
        this.size = size;
        population = new GP[size];

        switch (options.getCREATION_TYPE()) {
        case Options.GROW:
            for (int i = 0; i < population.length; i++) {
                population[i] =
                        new GP(environment, Options.GROW, options.getMAX_CREATION_DEPTH());
            }
            break;
        case Options.VARIABLE:
            for (int i = 0; i < population.length; i++) {
                population[i] =
                        new GP(environment, Options.VARIABLE, options.getMAX_CREATION_DEPTH());
            }
            break;
        case Options.RAMPED_HALF_AND_HALF:
            createRampedHalfAndHalf(size);
            break;
        }
    }*/
    
    private void createPopulation(int size) throws BNFException,
                                                   GeneException {
        this.size = size;
        population = new GP[size];
        firstGeneration= new ArrayList<GP>(size);
        ExecutorService executor = Executors.newFixedThreadPool(CPUs); //CPUs
        
        if (options.getCREATION_TYPE() != Options.RAMPED_HALF_AND_HALF) {
            for (int i = 0; i < size; i++) {
                executor.execute(new CreateGPTask(environment, options.getCREATION_TYPE(), options.getMAX_CREATION_DEPTH()));
            }
        }
        else {
            int nrOfGps = 0;
            int depth = options.getMAX_CREATION_DEPTH();
            for (int i = 1; i <= options.getMAX_CREATION_DEPTH() - 2; i++) {
                for (int j = 0; j < (size / (options.getMAX_CREATION_DEPTH() - 2));
                        j++) {
                    nrOfGps++;
                    if (Options.rnd.nextDouble() > 0.5) {
                        executor.execute(new CreateGPTask(environment, Options.VARIABLE, depth));
                    } else {
                        executor.execute(new CreateGPTask(environment, Options.GROW, depth));
                    }
                }
                depth--;
            }
            while (nrOfGps < size) {
                nrOfGps++;
                if (Options.rnd.nextDouble() > 0.5) {
                    executor.execute(new CreateGPTask(environment, Options.VARIABLE, options.getMAX_CREATION_DEPTH()));
                } else {
                    executor.execute(new CreateGPTask(environment, Options.GROW, options.getMAX_CREATION_DEPTH()));
                }
            }
        }
        
        
       
        
        
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.HOURS);
        } catch (InterruptedException ex) {
            ErrorManager.getInstance().reportError(ex);
        }
        for(int i=0;firstGeneration.size()<size;i++){
            if (options.getCREATION_TYPE() != Options.RAMPED_HALF_AND_HALF)
               new CreateGPTask(environment, options.getCREATION_TYPE(), options.getMAX_CREATION_DEPTH()).run();
            else
               new CreateGPTask(environment, Options.VARIABLE, options.getMAX_CREATION_DEPTH()).run();
        }
        for (int i = 0; i < population.length; i++) {
            population[i] = firstGeneration.get(i);
            
        }
    }

    public void createRampedHalfAndHalf(int size) throws BNFException,
                                                         GeneException {
        int nrOfGps = 0;
        int depth = options.getMAX_CREATION_DEPTH();
        for (int i = 1; i <= options.getMAX_CREATION_DEPTH() - 2; i++) {
            for (int j = 0; j < (size / (options.getMAX_CREATION_DEPTH() - 2));
                 j++) {
                if (Options.rnd.nextDouble() > 0.5)
                    population[nrOfGps++] =
                            new GP(environment, Options.VARIABLE, depth);
                else
                    population[nrOfGps++] =
                            new GP(environment, Options.GROW, depth);
            }
            depth--;
        }
        while (nrOfGps < size) {
            if (Options.rnd.nextDouble() > 0.5)
                population[nrOfGps++] =
                        new GP(environment, Options.VARIABLE, options.getMAX_CREATION_DEPTH());
            else
                population[nrOfGps++] =
                        new GP(environment, Options.GROW, options.getMAX_CREATION_DEPTH());
        }

    }

    public void evolvePopulation() throws GeneException, BNFException {
        population = createNextGeneration();
        if(options.getPROBLEM_TYPE() == Options.REGRESSION)
            performLokalRegressionMutationOnPopulation();
        calcPopulationFitness();
    }

    private void performLokalRegressionMutationOnPopulation(){
        for(int i = 1; i < population.length; i++){
            doLokalMutation(population[i].head);
            population[i].head.notifyChildrenOfModification();
        }
    }

    private void doLokalMutation(Node g){
        if(g==null)
            return;
        if(g instanceof TargetPred){
            if(Options.rnd.nextDouble()<options.getPARAMETER_1()){
                TargetPred tp = (TargetPred) g;
                double mutation = options.getPARAMETER_2() - Options.rnd.nextDouble()*options.getPARAMETER_2()*2;
                mutation += tp.getRegInternalValue();
                mutation = Math.max(mutation, 0);
                mutation = Math.min(mutation, 1);
                tp.setRegInternalValue(mutation);
                g.setModified(true);
                g.notifyParentsOfModification();
            }
            return;
        }
      /*  if(g instanceof Val){
            if(Options.rnd.nextDouble()<options.getPARAMETER_1()){
                Val tp = (Val) g;
                double mutation = options.getPARAMETER_2() - Options.rnd.nextDouble()*options.getPARAMETER_2()*2;
                mutation += tp.getValValue();
                mutation = Math.max(mutation, 0.001);
                mutation = Math.min(mutation, 0.999);
                tp.setValValue(mutation);
                g.setModified(true);
                g.notifyParentsOfModification();
            }
            return;
        }//*/
        if(g.getChildren()==null)
            return;
        for(Node child:g.getChildren())
            doLokalMutation(child);
    }

    private int iPop = 0;

    private ParsimonyPressure parsimonyPressure= new ParsimonyPressure();
    double pressure = 0;
    boolean activePressure = false;
    private void calcPopulationFitness() throws GeneException, BNFException {
      /*  population[0].calcAdjustedFitness();
        int s = population[0].calcLength(population[0].getHead());
        double acc =  population[0].getTrainAcc();
        
        System.out.println(population[0].getFitness() + " " +acc + " Size:" + s);

        population[0].calcAdjustedFitness();
        int s2 = population[0].calcLength(population[0].getHead());
        double acc2 =  population[0].getTrainAcc();
        System.out.println(population[0].getFitness() + " " + acc2+ " Size:" + s2);
        population[0].calcAdjustedFitness();
        int s3 = population[0].calcLength(population[0].getHead());
        double acc3 =  population[0].getTrainAcc();
        System.out.println(population[0].getFitness() + " " + acc3+ " Size:" + s3);


        System.out.println("");//*/
        populationLength = 0;
        populationFitness = 0;
        //double pressure =  parsimonyPressure.calcParsimonyPressure(population, 30); 

       // options.setPUNISHMENT_FOR_LENGTH(0);
        //ExecutorService executor = Executors.newCachedThreadPool();
        ExecutorService executor = Executors.newFixedThreadPool(CPUs); //CPUs
        for (int i = 0; i < population.length; i++) {
            executor.execute(population[i].getAdjustedFitnessTask());
        }
                      
        
        
        
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.HOURS);
        } catch (InterruptedException ex) {
            ErrorManager.getInstance().reportError(ex);
        }//*/
        
       /* for (int i = 0; i < population.length; i++){
            population[i].getAdjustedFitnessTask().run();
        }*/
       /* 
        double mLen = calcMeanLength();
        double mFit = calcMeanFitness();
        double coVar = calcCoVariance(population,mLen,mFit);
        double lenVar = calcLengthVariance(population,mLen);
        double tLen = 30;
        double tDiff = (tLen - mLen);
        double c = coVar/lenVar;
        System.out.println("tDiff: " + tDiff +" CoVar: " + coVar + " lenVar: " + lenVar + " c: " + c);
        if(activePressure ||  mLen >= tLen){
            if(!activePressure){
                System.out.println("Active");
                activePressure = true;
            }
            
            
            
//            double c = (-coVar - tDiff * mFit) / (lenVar - tDiff *mLen);
            

            for (int i = 0; i < population.length; i++) {
                GP gp = population[i];
                double f = gp.getFitness();
                double pp = gp.getLength()*c;
                f+= pp;
                gp.setFitness(f);
     //       }
            }
        }//*/
        
        
        for (int i = 0; i < population.length; i++) {
            populationFitness += population[i].getFitness();
            populationLength += population[i].getNrOfNodes();
        }
            
        populationInvertedFitness = population.length*100 - populationFitness;
        
     //   pressure = parsimonyPressure.calcParsimonyPressure(population,20);
    }
    
    private double calcMeanFitness(){
        double fitness =0;
        for(int i = 0; i < population.length;i++){
            fitness += population[i].getFitness();
        }
        return fitness / population.length;
    }
    
    private double calcMeanLength(){
        double length =0;
        for(int i = 0; i < population.length;i++){
            length += population[i].getNrOfNodes();
        }
        return length / population.length;
    }
    
    private double calcLengthVariance(GP[] pop, double meanLength){
         double var=0;
         for(int i = 0; i < pop.length;i++){
             var +=(pop[i].getNrOfNodes()-meanLength)*(pop[i].getNrOfNodes()-meanLength);
         }         
         return var/pop.length;
     }
    
    private double calcSingleSizeVariance(GP[] pop){
         Set<Double> s = new HashSet<Double>();
         double sum=0;
         for(int i = 0; i < pop.length;i++){
             if(s.add(pop[i].getNrOfNodes())){
                 sum+=pop[i].getNrOfNodes();
             }
         }
         double mean = sum / s.size();
         sum=0;
         for(Double d:s){
             sum+=(d-mean)*(d-mean)/s.size();
         }
         
         return sum;
     }
     
     
     private double calcCoVariance(GP[] pop, double meanLength, double meanFitness){
         double coVar=0;
         for(int i = 0; i < pop.length;i++){
             coVar +=(pop[i].getNrOfNodes()-meanLength)*(100-pop[i].getFitness()-100-meanFitness);
         }
         
         return coVar/(pop.length-1);
     }    
    
    public double getAvgPopulationFitness() {
        return populationFitness / population.length;
    }

    public double getPopulationLength() {
        return populationLength / population.length;
    }

    public GP selectBestInPopulation() {
        GP best = population[0];
        double max = best.getFitness();
        for (int i = 1; i < population.length; i++) {
            GP gp = population[i];
            if (gp.getFitness() < max) {
                max = gp.getFitness();
                best = gp;
            }
        }
        return best;
    }

    /**
     * Skapar en mating pool
     * @return GP[] matingpool
     * @throws GeneException
     */
    List<GP> matingpool;

    private void createMatingpool() throws GeneException {
        matingpool =
                Collections.synchronizedList(new ArrayList<GP>(population.length));
        int i = 0;
        if (population.length == 1) {
            matingpool.add(population[0]);
        } else {
            ExecutorService executor;
            if (options.getNR_OF_TOURNAMNET_MEMBERS()
                    > 0) { //Tournamnet Selection
                //executor = Executors.newFixedThreadPool(CPUs);
                for (int g = 0; g < population.length; g++) { 
                  //  executor.execute(new Runnable() {

                   //     public void run() {
                            tournamnetSelection();
                  //      }
                  //  });
                }
            } else {
           //     executor = Executors.newFixedThreadPool(CPUs);
                for (int g = 0; g < population.length;
                        g++) { //Roulette wheel selection
             //       executor.execute(new Runnable() {

               //         public void run() {
                            rouletteWheelSelection();
                //        }
               //     });
                }


            } //*/
           /* executor.shutdown();

            try {
                executor.awaitTermination(1, TimeUnit.HOURS);
            } catch (InterruptedException e) {
                ErrorManager.getInstance().reportError(e);
            }//*/
        }
    }
    //Todo Fix does not work propperly (mambe due to similar values
    private void rouletteWheelSelection() {
        double rnd = Options.rnd.nextDouble();
        double sum = 0;
        GP winner = null;
            for (GP gP : population) {
                sum += calcProportionalFitness(gP);
                if (sum > rnd){
                    winner = gP;
                    break;
                }
            }

        matingpool.add((GP)winner.clone());
    }

    private double calcProportionalFitness(GP gp) {
        return (100 - gp.getFitness()) / populationInvertedFitness;
    }

    private void tournamnetSelection() {
        int tournamentWinner =Options.rnd.nextInt(population.length);
            
        for (int t = 0; t < options.getNR_OF_TOURNAMNET_MEMBERS(); t++) {
            int challenger = Options.rnd.nextInt(population.length);
            if(population[tournamentWinner].getFitness() > population[challenger].getFitness())
                    tournamentWinner = challenger;
        }
        matingpool.add((GP)population[tournamentWinner].clone());
    }

    public void notifyGPofDataChange(){
        for(GP gp:population)
            gp.getHead().notifyChildrenOfModification();
    }


    public GP[] createNextGeneration() throws GeneException, BNFException {
        ArrayList<GP> nextGen = new ArrayList<GP>();

        GP[] nextGeneration = new GP[population.length];
        createMatingpool();
        if (options.isELITISM()) {

            nextGen.add((GP)this.getBestGP().clone());
            matingpool.remove(0);

        }


        // Chansen for att ett Gp skall voljas skall lika med Cross_Prob
        // tvo Gp vols dock vid varje crossover alltso / 2
        int nrOfCrossowers =
            (int)Math.floor(matingpool.size() * options.getCROSSOVER_PROBABILITY() /
                            2);
        if (population.length == 1)
            nrOfCrossowers = 0;
        for (int j = 0; j < nrOfCrossowers; j++) {
            int index1 = matingpool.size() - 1;
            int index2 = matingpool.size() - 2;

            GP gp1 = (GP)matingpool.get(index1);
            GP gp2 = (GP)matingpool.get(index2);
            crossover(gp1, gp2);

            nextGen.add(gp1);
            nextGen.add(gp2);
            //gp1.getHead().setModified(true);
           // gp2.getHead().setModified(true);
            matingpool.remove(index1);
            matingpool.remove(index2);
        }
        int nrOfMutation =
            (int)Math.floor(matingpool.size() * options.getMUTATION_PROBABILITY() /
                            2);
        for (int m = 0; m < nrOfMutation; m++) {
            mutate(matingpool.get(matingpool.size() - 1));

          //  matingpool.get(matingpool.size() - 1).getHead().setModified(true);

        }
        for (GP gpi : matingpool) {//Reproduction
            nextGen.add(gpi);        
        }
        matingpool = null;
        //return nextGeneration;
        return nextGen.toArray(nextGeneration);
    }

    /**@todo
     * Implementera greedy OverSelection for populationer over 500
     */
    private GP reproduction(GP gp) {
        GP newGP = (GP)gp.clone();
        return newGP;
    }

    public GP getBestGP() {
        GP tmp = population[0];
        for (int i = 1; i < population.length; i++) {
            tmp =
                    tmp.getFitness() <= population[i].getFitness() ? tmp : population[i];
        }
        return tmp;
    }


    public boolean performCrossover(Node parent1,
                                    Node parent2) throws BNFException {
        Node[] g1Childs = parent1.getChildren();
        Node[] g2Childs = parent2.getChildren();
        if (g1Childs != null && g2Childs != null) {
            int g1Index = Options.rnd.nextInt(g1Childs.length);
            int g2Index = Options.rnd.nextInt(g2Childs.length);
            Node candidate1 = g1Childs[g1Index];
            Node candidate2 = g2Childs[g2Index];

            String[] g1Compatible =
                environment.getBNF().getCompatibleGenes(parent1, g1Index);
            String[] g2Compatible =
                environment.getBNF().getCompatibleGenes(parent2, g2Index);

            for (int i = 0; i < g1Compatible.length; i++) {
                if (g1Compatible[i].equals(candidate2.getNodeName())) {
                    for (int j = 0; j < g2Compatible.length; j++) {
                        if (g2Compatible[j].equals(candidate1.getNodeName())) {
                            g1Childs[g1Index] = candidate2;
                            candidate2.setParent(parent1);
                            g2Childs[g2Index] = candidate1;
                            candidate1.setParent(parent2);
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    /**@todo
     * Kolla maximumdetph bor vara mindre on 17 for att ej ge abnorma program
     * Se till so att 90% or funktioner och 10% terminaler vid crossover
     */
    public void mutate(GP gp) throws BNFException, GeneException {
        List gpList = gp.getTreeAsList();
        Node gene = (Node)gpList.get(Options.rnd.nextInt(gpList.size()));
        environment.getBNF().createVariableGene(gene,
                                                options.getMAX_CREATION_DEPTH());
        gene.notifyParentsOfModification();
        gene.notifyChildrenOfModification();
        gp.calcNrOfNodes();
    }

    private boolean crossover(GP gp1, GP gp2) throws BNFException {
        List gp1List = gp1.getTreeAsList();
        List gp2List = gp2.getTreeAsList();
        for (int i = 0; i < NR_OF_CROSSOVER_TRIES; i++) {
            Node gene1,gene2;
            
            if(gp1List.size()==1)
                gene1 = (Node)gp1List.get(0);
            else
                gene1 = (Node)gp1List.get(Options.rnd.nextInt(gp1List.size()-1)+1);
            if(gp2List.size()==1)
                gene2 = (Node)gp2List.get(0);
            else
                gene2 = (Node)gp2List.get(Options.rnd.nextInt(gp2List.size()-1)+1);
            
            if (performCrossover(gene1, gene2)) {
                gene1.notifyChildrenOfModification();
                gene2.notifyChildrenOfModification();
                gene1.notifyParentsOfModification();
                gene2.notifyParentsOfModification();
                gp1.calcNrOfNodes();
                gp2.calcNrOfNodes();
                return true;
            }
        }
        return false;
    }

    public String toString() {
        String tmp = "";
        for (int i = 0; i < population.length; i++) {
            tmp += population[i] + "\n";
        }
        return tmp;
    }

    public void destroyGPs() {
        population = null;
        System.gc();
    }

}
